using System;
using System.Configuration;
using System.Configuration.Provider;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Configuration;
using System.Web.Security;

namespace Genrsis.Web.Security
{
    /*
    * See http://msdn.microsoft.com/en-us/library/aa479031.aspx for implementation details.
    */
    public class AspNetSqlMembershipProvider : MembershipProvider
    {
        private string connectionString;
        private bool enablePasswordRetrieval;
        private bool enablePasswordReset;
        private bool requiresQuestionAndAnswer;
        private int maxInvalidPasswordAttempts;
        private int passwordAttemptWindow;
        private bool requiresUniqueEmail;
        private MembershipPasswordFormat passwordFormat;
        private int minRequiredPasswordLength;
        private int minRequiredNonAlphanumericCharacters;
        private string passwordStrengthRegularExpression = "^.*$";
        private MachineKeySection machineKey;

        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            if (string.IsNullOrEmpty(name))
            {
                name = "AspNetSqlMembershipProvider";
            }

            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Custom SQL Membership provider");
            }

            base.Initialize(name, config);

            ApplicationName = !string.IsNullOrEmpty(config["applicationName"]) ? config["applicationName"] : System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;
            maxInvalidPasswordAttempts = Convert.ToInt32(!string.IsNullOrEmpty(config["maxInvalidPasswordAttempts"]) ? config["maxInvalidPasswordAttempts"] : "5");
            passwordAttemptWindow = Convert.ToInt32(!string.IsNullOrEmpty(config["passwordAttemptWindow"]) ? config["passwordAttemptWindow"] : "10");
            minRequiredNonAlphanumericCharacters = Convert.ToInt32(!string.IsNullOrEmpty(config["minRequiredNonAlphanumericCharacters"]) ? config["minRequiredNonAlphanumericCharacters"] : "1");
            minRequiredPasswordLength = Convert.ToInt32(!string.IsNullOrEmpty(config["minRequiredPasswordLength"]) ? config["minRequiredPasswordLength"] : "7");
            passwordStrengthRegularExpression = !string.IsNullOrEmpty(config["passwordStrengthRegularExpression"]) ? config["passwordStrengthRegularExpression"] : "";
            enablePasswordReset = Convert.ToBoolean(!string.IsNullOrEmpty(config["enablePasswordReset"]) ? config["enablePasswordReset"] : "true");
            enablePasswordRetrieval = Convert.ToBoolean(!string.IsNullOrEmpty(config["enablePasswordRetrieval"]) ? config["enablePasswordRetrieval"] : "true");
            requiresQuestionAndAnswer = Convert.ToBoolean(!string.IsNullOrEmpty(config["requiresQuestionAndAnswer"]) ? config["requiresQuestionAndAnswer"] : "true");
            requiresUniqueEmail = Convert.ToBoolean(!string.IsNullOrEmpty(config["requiresUniqueEmail"]) ? config["requiresUniqueEmail"] : "true");

            string passwordFormatString = config["passwordFormat"] ?? "Hashed";

            passwordFormat = (MembershipPasswordFormat)Enum.Parse(typeof(MembershipPasswordFormat), passwordFormatString);

            var connectionStringSettings = ConfigurationManager.ConnectionStrings[config["connectionStringName"]];

            if (connectionStringSettings == null || connectionStringSettings.ConnectionString.Trim() == "")
            {
                throw new ProviderException("Connection string cannot be blank.");
            }

            connectionString = connectionStringSettings.ConnectionString;

            // Get encryption and decryption key information from the configuration.
            Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey");

            if (machineKey.ValidationKey.Contains("AutoGenerate"))
            {
                if (PasswordFormat != MembershipPasswordFormat.Clear)
                {
                    throw new ProviderException("Hashed or Encrypted passwords are not supported with auto-generated keys.");
                }
            }
        }

        /// <summary>
        /// Adds a new membership user to the data source.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser" /> object populated with the information for the newly created user.
        /// </returns>
        /// <param name="username">
        /// The user name for the new user. 
        /// </param>
        /// <param name="password">
        /// The password for the new user. 
        /// </param>
        /// <param name="email">
        /// The e-mail address for the new user.
        /// </param>
        /// <param name="passwordQuestion">
        /// The password question for the new user.
        /// </param>
        /// <param name="passwordAnswer">
        /// The password answer for the new user
        /// </param>
        /// <param name="isApproved">
        /// Whether or not the new user is approved to be validated.
        /// </param>
        /// <param name="providerUserKey">
        /// The unique identifier from the membership data source for the user.
        /// </param>
        /// <param name="status">
        /// A <see cref="T:System.Web.Security.MembershipCreateStatus" /> enumeration value indicating whether the user was created successfully.
        /// </param>
        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            var args = new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if (RequiresQuestionAndAnswer)
            {
                if (string.IsNullOrEmpty(passwordQuestion))
                {
                    status = MembershipCreateStatus.InvalidQuestion;
                    return null;
                }

                if (string.IsNullOrEmpty(passwordAnswer))
                {
                    status = MembershipCreateStatus.InvalidAnswer;
                    return null;
                }
            }

            using (var context = GetContext())
            {
                if (RequiresUniqueEmail)
                {
                    // Look for a user with this email address
                    var query = from u in GetApplicationUsers(context)
                                where u.EmailAddress == email
                                select u;
                    if (query.Any())
                    {
                        status = MembershipCreateStatus.DuplicateEmail;
                        return null;
                    }
                }

                var user = new User
                {
                    DisplayName = username,
                    EmailAddress = email,
                    Username = username,
                    IsApproved = true,
                    Password = EncodePassword(password),
                    Comment = "",
                    CreationDate = DateTime.Now,
                    IsLockedOut = false,
                    LastActivityDate = DateTime.Now,
                    LastLockoutDate = null,
                    LastLoginDate = null,
                    FailedPasswordAnswerAttemptWindowStart = null,
                    FailedPasswordAttemptWindowStart = null,
                    ApplicationName = ApplicationName,
                    LastPasswordChangedDate = DateTime.Now,
                    PasswordQuestion = passwordQuestion,
                    ProviderName = "AspNetSqlMembershipProvider"
                };
                context.Users.InsertOnSubmit(user);

                context.SubmitChanges();

                // TODO: Different statuses based on some validation on user creation
                status = MembershipCreateStatus.Success;
                return new MembershipUser(user.ProviderName, user.Username, user.ID, user.EmailAddress, user.PasswordQuestion, user.Comment, user.IsApproved, user.IsLockedOut, user.CreationDate, default(DateTime), DateTime.Now, DateTime.Now, default(DateTime));
            }
        }

        /// <summary>
        /// Processes a request to update the password question and answer for a membership user.
        /// </summary>
        /// <returns>
        /// true if the password question and answer are updated successfully; otherwise, false.
        /// </returns>
        /// <param name="username">
        /// The user to change the password question and answer for. 
        /// </param>
        /// <param name="password">
        /// The password for the specified user. 
        /// </param>
        /// <param name="newPasswordQuestion">
        /// The new password question for the specified user. 
        /// </param>
        /// <param name="newPasswordAnswer">
        /// The new password answer for the specified user. 
        /// </param>
        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            if (ValidateUser(username, password) == false) return false;

            using (var context = GetContext())
            {
                User user = FindUser(context, username);

                if (user == null)
                {
                    throw new MembershipException(string.Format("User {0} not found with the supplied password", username));
                }

                user.PasswordQuestion = newPasswordQuestion;
                user.PasswordAnswer = newPasswordAnswer;

                context.SubmitChanges();
            }

            return true;
        }

        /// <summary>
        /// Gets the password for the specified user name from the data source.
        /// </summary>
        /// <returns>
        /// The password for the specified user name.
        /// </returns>
        /// <param name="username">
        /// The user to retrieve the password for. 
        /// </param>
        /// <param name="answer">
        /// The password answer for the user. 
        /// </param>
        public override string GetPassword(string username, string answer)
        {
            if (!EnablePasswordRetrieval)
            {
                throw new ProviderException("Password Retrieval Not Enabled.");
            }

            if (PasswordFormat == MembershipPasswordFormat.Hashed)
            {
                throw new ProviderException("Cannot retrieve Hashed passwords.");
            }

            using (var context = GetContext())
            {
                var user = FindUser(context, username);

                if (user == null) throw new ProviderException(string.Format("User {0} not found.", username));

                if (user.IsLockedOut) throw new ProviderException(string.Format("User {0} is locked out.", username));

                if (RequiresQuestionAndAnswer && !CheckPassword(answer, user.PasswordAnswer))
                {
                    UpdateFailureCount(username, "passwordAnswer");

                    throw new MembershipPasswordException("Incorrect password answer.");
                }

                string ret = user.Password;

                if (PasswordFormat == MembershipPasswordFormat.Encrypted)
                {
                    ret = UnEncodePassword(ret);
                }

                return ret;
            }
        }

        /// <summary>
        /// Processes a request to update the password for a membership user.
        /// </summary>
        /// <returns>
        /// true if the password was updated successfully; otherwise, false.
        /// </returns>
        /// <param name="username">
        /// The user to update the password for. 
        /// </param>
        /// <param name="oldPassword">
        /// The current password for the specified user. 
        /// </param>
        /// <param name="newPassword">
        /// The new password for the specified user. 
        /// </param>
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            if (ValidateUser(username, oldPassword) == false) return false;

            var args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                if (args.FailureInformation != null)
                {
                    throw args.FailureInformation;
                }
                else
                {
                    throw new MembershipPasswordException("Change password canceled due to new password validation failure.");
                }
            }


            using (var context = GetContext())
            {
                User user = FindUser(context, username);

                if (user == null)
                {
                    throw new MembershipException(string.Format("User {0} not found with the supplied password", username));
                }

                user.Password = newPassword;
                user.LastPasswordChangedDate = DateTime.Now;

                context.SubmitChanges();
            }

            return true;
        }

        /// <summary>
        /// Resets a user's password to a new, automatically generated password.
        /// </summary>
        /// <returns>
        /// The new password for the specified user.
        /// </returns>
        /// <param name="username">
        /// The user to reset the password for. 
        /// </param>
        /// <param name="answer">
        /// The password answer for the specified user. 
        /// </param>
        public override string ResetPassword(string username, string answer)
        {
            if (EnablePasswordReset == false) throw new NotSupportedException("Password reset is not enabled.");

            if (string.IsNullOrEmpty(answer) && RequiresQuestionAndAnswer)
            {
                UpdateFailureCount(username, "passwordAnswer");

                throw new ProviderException("Password answer required for password reset.");
            }

            string newPassword = Membership.GeneratePassword(MinRequiredPasswordLength, MinRequiredNonAlphanumericCharacters);

            var args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                if (args.FailureInformation != null)
                {
                    throw args.FailureInformation;
                }
                else
                {
                    throw new MembershipPasswordException("Reset password canceled due to password validation failure.");
                }
            }

            using (var context = GetContext())
            {
                User user = FindUser(context, username);

                if (user == null) throw new MembershipException(string.Format("User {0} is not found", username));

                if (user.IsLockedOut) throw new MembershipException(string.Format("User {0} is locked out", username));

                if (RequiresQuestionAndAnswer && !CheckPassword(answer, user.PasswordAnswer))
                {
                    UpdateFailureCount(username, "passwordAnswer");

                    throw new MembershipPasswordException("Incorrect password answer.");
                }

                user.Password = newPassword;
                user.LastPasswordChangedDate = DateTime.Now;

                context.SubmitChanges();

                return newPassword;
            }
        }

        /// <summary>
        /// Updates information about a user in the data source.
        /// </summary>
        /// <param name="membershipUser">
        /// A <see cref="T:System.Web.Security.MembershipUser" /> object that represents the user to update and the updated information for the user. 
        /// </param>
        public override void UpdateUser(MembershipUser membershipUser)
        {
            using (var context = GetContext())
            {
                User user = FindUser(context, membershipUser.UserName);

                if (user == null)
                {
                    throw new MembershipException(string.Format("User {0} not found", membershipUser.UserName));
                }

                user.Comment = membershipUser.Comment;
                user.EmailAddress = membershipUser.Email;
                user.IsApproved = membershipUser.IsApproved;
                user.IsLockedOut = membershipUser.IsLockedOut;
                user.LastActivityDate = membershipUser.LastActivityDate;
                user.LastLockoutDate = membershipUser.LastLockoutDate;
                user.LastLoginDate = membershipUser.LastLoginDate;
                user.LastPasswordChangedDate = membershipUser.LastPasswordChangedDate;
                user.PasswordQuestion = membershipUser.PasswordQuestion;
                user.ProviderName = membershipUser.ProviderName;
                user.Username = membershipUser.UserName;

                context.SubmitChanges();
            }
        }

        /// <summary>
        /// Verifies that the specified user name and password exist in the data source.
        /// </summary>
        /// <returns>
        /// true if the specified username and password are valid; otherwise, false.
        /// </returns>
        /// <param name="username">
        /// The name of the user to validate. 
        /// </param>
        /// <param name="password">
        /// The password for the specified user. 
        /// </param>
        public override bool ValidateUser(string username, string password)
        {
            using (var context = GetContext())
            {
                User user = FindUser(context, username);

                if (user == null) return false;

                if (user.IsLockedOut || user.IsApproved == false) return false;

                bool isValid;

                if (CheckPassword(password, user.Password))
                {
                    user.LastLoginDate = DateTime.Now;
                    isValid = true;
                }
                else
                {
                    UpdateFailureCount(username, "password");
                    isValid = false;
                }

                context.SubmitChanges();
                //try
                //{
                //    context.SubmitChanges(ConflictMode.ContinueOnConflict);
                //}
                //catch (ChangeConflictException e)
                //{
                //    Console.WriteLine(e.Message);
                //    // Automerge database values for members that client
                //    // has not modified.
                //    foreach (ObjectChangeConflict occ in context.ChangeConflicts)
                //    {
                //        occ.Resolve(RefreshMode.KeepChanges);
                //    }
                //}

                return isValid;
            }
        }

        /// <summary>
        /// Clears a lock so that the membership user can be validated.
        /// </summary>
        /// <returns>
        /// true if the membership user was successfully unlocked; otherwise, false.
        /// </returns>
        /// <param name="username">
        /// The membership user whose lock status you want to clear.
        /// </param>
        public override bool UnlockUser(string username)
        {
            using (var context = GetContext())
            {
                User user = FindUser(context, username);
                user.IsLockedOut = false;
                user.LastLockoutDate = null;

                context.SubmitChanges();
            }

            return true;
        }

        /// <summary>
        /// Gets user information from the data source based on the unique identifier for the membership user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser" /> object populated with the specified user's information from the data source.
        /// </returns>
        /// <param name="providerUserKey">
        /// The unique identifier for the membership user to get information for.
        /// </param>
        /// <param name="userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user.
        /// </param>
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            int userId = (int)providerUserKey;

            using (var context = GetContext())
            {
                User user = FindUser(context, userId);

                if (user == null) throw new MembershipException(string.Format("User {0} not found.", userId));

                if (userIsOnline)
                {
                    user.LastActivityDate = DateTime.Now;
                    context.SubmitChanges();
                }

                return UserToMembershipUser(user);
            }
        }

        /// <summary>
        /// Gets information from the data source for a user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser" /> object populated with the specified user's information from the data source.
        /// </returns>
        /// <param name="username">
        /// The name of the user to get information for. 
        /// </param>
        /// <param name="userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user. 
        /// </param>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            using (var context = GetContext())
            {
                User user = FindUser(context, username);

                if (user == null) throw new MembershipException(string.Format("User {0} not found.", username));

                if (userIsOnline)
                {
                    user.LastActivityDate = DateTime.Now;
                    context.SubmitChanges();
                }

                return UserToMembershipUser(user);
            }
        }

        /// <summary>
        /// Gets the user name associated with the specified e-mail address.
        /// </summary>
        /// <returns>
        /// The user name associated with the specified e-mail address. If no match is found, return null.
        /// </returns>
        /// <param name="email">
        /// The e-mail address to search for. 
        /// </param>
        public override string GetUserNameByEmail(string email)
        {
            using (var context = GetContext())
            {
                return (from u in GetApplicationUsers(context)
                        where u.EmailAddress == email
                        select u.Username).SingleOrDefault();
            }
        }

        /// <summary>
        /// Removes a user from the membership data source. 
        /// </summary>
        /// <returns>
        /// true if the user was successfully deleted; otherwise, false.
        /// </returns>
        /// <param name="username">
        /// The name of the user to delete.
        /// </param>
        /// <param name="deleteAllRelatedData">true to delete data related to the user from the database; false to leave data related to the user in the database.
        /// </param>
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new System.NotImplementedException();
        }

        /// <summary>
        /// Gets a collection of all the users in the data source in pages of data.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection" /> collection that contains a page of <paramref name="pageSize" /><see cref="T:System.Web.Security.MembershipUser" /> objects beginning at the page specified by <paramref name="pageIndex" />.
        /// </returns>
        /// <param name="pageIndex">
        /// The index of the page of results to return. <paramref name="pageIndex" /> is zero-based.
        /// </param>
        /// <param name="pageSize">
        /// The size of the page of results to return.
        /// </param>
        /// <param name="totalRecords">
        /// The total number of matched users.
        /// </param>
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            int skip = pageIndex * pageSize;

            using (var context = GetContext())
            {
                var query = (from u in GetApplicationUsers(context)
                             where u.ApplicationName == ApplicationName
                             orderby u.Username
                             select u).Skip(skip).Take(pageSize);

                var ret = new MembershipUserCollection();
                foreach (User user in query)
                {
                    ret.Add(UserToMembershipUser(user));
                }

                totalRecords = ret.Count;
                return ret;
            }
        }

        /// <summary>
        /// Gets the number of users currently accessing the application.
        /// </summary>
        /// <returns>
        /// The number of users currently accessing the application.
        /// </returns>
        public override int GetNumberOfUsersOnline()
        {
            var onlineSpan = new TimeSpan(0, Membership.UserIsOnlineTimeWindow, 0);
            DateTime compareTime = DateTime.Now.Subtract(onlineSpan);

            using (var context = GetContext())
            {
                return (from u in GetApplicationUsers(context)
                        where u.LastActivityDate >= compareTime
                        select u).Count();
            }
        }

        /// <summary>
        /// Gets a collection of membership users where the user name contains the specified user name to match.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection" /> collection that contains a page of <paramref name="pageSize" /><see cref="T:System.Web.Security.MembershipUser" /> objects beginning at the page specified by <paramref name="pageIndex" />.
        /// </returns>
        /// <param name="usernameToMatch">
        /// The user name to search for.
        /// </param>
        /// <param name="pageIndex">
        /// The index of the page of results to return. <paramref name="pageIndex" /> is zero-based.
        /// </param>
        /// <param name="pageSize">
        /// The size of the page of results to return.
        /// </param>
        /// <param name="totalRecords">
        /// The total number of matched users.
        /// </param>
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            int skip = pageIndex * pageSize;

            using (var context = GetContext())
            {
                var query = (from u in GetApplicationUsers(context)
                             where u.Username.Contains(usernameToMatch)
                             select u).Skip(skip).Take(pageSize);

                var ret = new MembershipUserCollection();
                foreach (User user in query)
                {
                    ret.Add(UserToMembershipUser(user));
                }

                totalRecords = ret.Count;
                return ret;
            }
        }

        /// <summary>
        /// Gets a collection of membership users where the e-mail address contains the specified e-mail address to match.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection" /> collection that contains a page of <paramref name="pageSize" /><see cref="T:System.Web.Security.MembershipUser" /> objects beginning at the page specified by <paramref name="pageIndex" />.
        /// </returns>
        /// <param name="emailToMatch">
        /// The e-mail address to search for.
        /// </param>
        /// <param name="pageIndex">
        /// The index of the page of results to return. <paramref name="pageIndex" /> is zero-based.
        /// </param>
        /// <param name="pageSize">
        /// The size of the page of results to return.
        /// </param>
        /// <param name="totalRecords">
        /// The total number of matched users.
        /// </param>
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            int skip = pageIndex * pageSize;

            using (var context = GetContext())
            {
                var query = (from u in GetApplicationUsers(context)
                             where u.EmailAddress.Contains(emailToMatch)
                             select u).Skip(skip).Take(pageSize);

                var ret = new MembershipUserCollection();
                foreach (User user in query)
                {
                    ret.Add(UserToMembershipUser(user));
                }

                totalRecords = ret.Count;
                return ret;
            }
        }

        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to retrieve their passwords.
        /// </summary>
        /// <returns>
        /// true if the membership provider is configured to support password retrieval; otherwise, false. The default is false.
        /// </returns>
        public override bool EnablePasswordRetrieval
        {
            get { return enablePasswordRetrieval; }
        }

        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to reset their passwords.
        /// </summary>
        /// <returns>
        /// true if the membership provider supports password reset; otherwise, false. The default is true.
        /// </returns>
        public override bool EnablePasswordReset
        {
            get { return enablePasswordReset; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval.
        /// </summary>
        /// <returns>
        /// true if a password answer is required for password reset and retrieval; otherwise, false. The default is true.
        /// </returns>
        public override bool RequiresQuestionAndAnswer
        {
            get { return requiresQuestionAndAnswer; }
        }

        /// <summary>
        /// The name of the application using the custom membership provider.
        /// </summary>
        /// <returns>
        /// The name of the application using the custom membership provider.
        /// </returns>
        public override string ApplicationName { get; set; }

        /// <summary>
        /// Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </summary>
        /// <returns>
        /// The number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </returns>
        public override int MaxInvalidPasswordAttempts
        {
            get { return maxInvalidPasswordAttempts; }
        }

        /// <summary>
        /// Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </summary>
        /// <returns>
        /// The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </returns>
        public override int PasswordAttemptWindow
        {
            get { return passwordAttemptWindow; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name.
        /// </summary>
        /// <returns>
        /// true if the membership provider requires a unique e-mail address; otherwise, false. The default is true.
        /// </returns>
        public override bool RequiresUniqueEmail
        {
            get { return requiresUniqueEmail; }
        }

        /// <summary>
        /// Gets a value indicating the format for storing passwords in the membership data store.
        /// </summary>
        /// <returns>
        /// One of the <see cref="T:System.Web.Security.MembershipPasswordFormat" /> values indicating the format for storing passwords in the data store.
        /// </returns>
        public override MembershipPasswordFormat PasswordFormat
        {
            get { return passwordFormat; }
        }

        /// <summary>
        /// Gets the minimum length required for a password.
        /// </summary>
        /// <returns>
        /// The minimum length required for a password. 
        /// </returns>
        public override int MinRequiredPasswordLength
        {
            get { return minRequiredPasswordLength; }
        }

        /// <summary>
        /// Gets the minimum number of special characters that must be present in a valid password.
        /// </summary>
        /// <returns>
        /// The minimum number of special characters that must be present in a valid password.
        /// </returns>
        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return minRequiredNonAlphanumericCharacters; }
        }

        /// <summary>
        /// Gets the regular expression used to evaluate a password.
        /// </summary>
        /// <returns>
        /// A regular expression used to evaluate a password.
        /// </returns>
        public override string PasswordStrengthRegularExpression
        {
            get { return passwordStrengthRegularExpression; }
        }

        private IQueryable<User> GetApplicationUsers(MembershipDataContext context)
        {
            return from u in context.Users
                   where u.ApplicationName == ApplicationName
                   select u;
        }

        private User FindUser(MembershipDataContext context, int ID)
        {
            return (from u in GetApplicationUsers(context)
                    where u.ID == ID
                    select u).SingleOrDefault();
        }

        private User FindUser(MembershipDataContext context, string username)
        {
            return (from u in GetApplicationUsers(context)
                    where u.Username == username
                    select u).SingleOrDefault();
        }

        private static MembershipUser UserToMembershipUser(User user)
        {
            return new MembershipUser(user.ProviderName, user.Username, user.ID, user.EmailAddress, user.PasswordQuestion, user.Comment, user.IsApproved, user.IsLockedOut, user.CreationDate,
                                         user.LastLoginDate ?? DateTime.Now, user.LastActivityDate ?? DateTime.Now, user.LastPasswordChangedDate ?? DateTime.Now, user.LastLockoutDate ?? DateTime.Now);
        }

        // TODO: Change the failure type to something better than a string
        private void UpdateFailureCount(string username, string failureType)
        {
            using (MembershipDataContext context = GetContext())
            {
                var user = FindUser(context, username);

                if (user == null) throw new ProviderException(string.Format("User not found: {0}", username));

                int failureCount = 0;
                DateTime? windowStart = DateTime.Now;

                switch (failureType)
                {
                    case "password":
                        failureCount = user.FailedPasswordAttemptCount;
                        windowStart = user.FailedPasswordAttemptWindowStart ?? DateTime.Now;
                        break;
                    case "passwordAnswer":
                        failureCount = user.FailedPasswordAnswerAttemptCount;
                        windowStart = user.FailedPasswordAnswerAttemptWindowStart ?? DateTime.Now;
                        break;
                }

                DateTime windowEnd = windowStart.Value.AddMinutes(PasswordAttemptWindow);

                if (failureCount == 0 || DateTime.Now > windowEnd)
                {
                    // First password failure or outside of PasswordAttemptWindow. 
                    // Start a new password failure count from 1 and a new window starting now.
                    switch (failureType)
                    {
                        case "password":
                            user.FailedPasswordAttemptCount = failureCount;
                            user.FailedPasswordAttemptWindowStart = windowStart;
                            break;
                        case "passwordAnswer":
                            user.FailedPasswordAnswerAttemptCount = failureCount;
                            user.FailedPasswordAnswerAttemptWindowStart = windowStart;
                            break;
                    }
                }
                else
                {
                    // Plus one failure...
                    failureCount++;

                    if (failureCount >= MaxInvalidPasswordAttempts)
                    {
                        // Password attempts have exceeded the failure threshold. Lock out
                        // the user.
                        user.IsLockedOut = true;
                        user.LastLockoutDate = DateTime.Now;
                    }
                    else
                    {
                        // Password attempts have not exceeded the failure threshold. Update
                        // the failure counts. Leave the window the same.
                        switch (failureType)
                        {
                            case "password":
                                user.FailedPasswordAttemptCount = failureCount;
                                break;
                            case "passwordAnswer":
                                user.FailedPasswordAnswerAttemptCount = failureCount;
                                break;
                        }
                    }
                }

                context.SubmitChanges();
            }
        }

        private bool CheckPassword(string password, string dbpassword)
        {
            string pass1 = password;
            string pass2 = dbpassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Encrypted:
                    pass2 = UnEncodePassword(dbpassword);
                    break;
                case MembershipPasswordFormat.Hashed:
                    pass1 = EncodePassword(password);
                    break;
                default:
                    break;
            }

            if (pass1 == pass2)
            {
                return true;
            }

            return false;
        }


        //
        // EncodePassword
        //   Encrypts, Hashes, or leaves the password clear based on the PasswordFormat.
        //

        private string EncodePassword(string password)
        {
            string encodedPassword = password;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    encodedPassword =
                      Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    HMACSHA1 hash = new HMACSHA1();
                    hash.Key = HexToByte(machineKey.ValidationKey);
                    encodedPassword =
                      Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return encodedPassword;
        }


        //
        // UnEncodePassword
        //   Decrypts or leaves the password clear based on the PasswordFormat.
        //

        private string UnEncodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    password =
                      Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    throw new ProviderException("Cannot unencode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        //
        // HexToByte
        //   Converts a hexadecimal string to a byte array. Used to convert encryption
        // key values from the configuration.
        //

        private static byte[] HexToByte(string hexString)
        {
            byte[] returnBytes = new byte[hexString.Length / 2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
            return returnBytes;
        }

        private MembershipDataContext GetContext()
        {
            return new MembershipDataContext(connectionString);
        }
    }
}